package it.tukano.io;

import it.tukano.check.Checks;
import it.tukano.collections.LinkedListExt;
import it.tukano.collections.ListExt;
import it.tukano.fun.Function1;
import it.tukano.fun.Function2;
import it.tukano.fun.VFunction1;
import java.awt.Desktop;
import java.beans.XMLDecoder;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.text.Collator;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Properties;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Wraps a file to provide some utilities
 * @author pgi
 */
public class FileExt {
    

    /* The wrapped file */
    private final File file;
    
    /**
     * Creates a new file ext wrapping the given file
     * @param file
     */
    public FileExt(File file) {
        this.file = file;
    }
    
    /**
     * A file in a directory
     * @param parent the parent file
     * @param file the file name or relative path
     */
    public FileExt(File parent, String file) {
        this.file = new File(parent, file);
    }
    
    public FileExt(FileExt parent, String fileName) {
        this(parent.getFile(), fileName);
    }    
    
    /**
     * Sort by full file path
     */
    public static Function2<FileExt, FileExt, Integer> PATH_ORDER = new Function2<FileExt, FileExt, Integer>() {

        public Integer apply(FileExt a, FileExt b) {
            return Collator.getInstance().compare(a.file.getPath(), b.file.getPath());
        }
    };
    
    /**
     * Sort by file name
     */
    public static Function2<FileExt, FileExt, Integer> NAME_ORDER = new Function2<FileExt, FileExt, Integer>() {

        public Integer apply(FileExt a, FileExt b) {
            return Collator.getInstance().compare(a.file.getName(), b.file.getName());
        }
    };

    /**
     * Creates a temporary file in the user home folder with some extension
     * @param extension the extension of the file
     * @return the file
     */
    public static FileExt createTempFile(String extension) {
        if(!extension.startsWith(".")) extension = "." + extension;
        final String home = System.getProperty("user.home");
        final Random random = new Random();
        File file = null;
        do {
            file = new File(home, "TEMP" + random.nextLong() + extension);
        } while(file.exists());
        file.deleteOnExit();
        return new FileExt(file);
    }

    /**
     * Returns the root folder of the given class (file)
     * @param aClass the target class
     * @return the path of the folder that contains the root of the package of the given class or
     * the folder that contains the jar file if the class is contained in a jar file
     */
    public static FileExt getJarFolder(Class<?> aClass) {
        String name = aClass.getName();
        name = "/" + name.replace(".", "/") + ".class";
        URL url = FileExt.class.getResource(name);
        String path = url.toString();
        path = path.replace("%20", " ");
        if(path.startsWith("jar:file:/")) {
            path = path.substring("jar:file:/".length());
            int i = path.indexOf("!");
            path = path.substring(0, i);
            File file = new File(path);
            return new FileExt(file.getParentFile());
        } else if(path.startsWith("file:/")) {
            path = path.substring("file:/".length(), path.length() - name.length());
            File file = new File(path);
            return new FileExt(file);
        }
        return null;
    }
    
    /** Returns the name of the underlying file (or null is file is null) */
    public String getName() {
        return file == null ? "null" : file.getName();
    }

    /**
     * Deletes the underlying file
     */
    public void delete() {
        this.file.delete();
    }

    /**
     * Returns the uri of the file or null if an exception is thrown
     * @return the uri of the file
     */
    public URI toURI() {
        try {
            return getURL().toURI();
        } catch (URISyntaxException ex) {
            return null;
        }
    }

    /**
     * Opens this file with some program
     * @param executablePath the path of the program to use to open the file
     * @return the created process or null if some exception happens
     */
    public Process openWith(String executablePath) {
        ProcessBuilder pb = new ProcessBuilder(executablePath, file.getPath());
        try {
            return pb.start();
        } catch (IOException ex) {
            Logger.getLogger(FileExt.class.getName()).log(Level.SEVERE, null, ex);
        }
        return null;
    }

    /**
     * Checks if the underlying file exists
     * @return true if the file exists
     */
    public boolean exists() {
        return file != null && file.exists();
    }

    /**
     * Read the content of this file as a java.util.Properties
     * @return the properties or null if the file cannot be read
     */
    public Properties getContentAsProperties() {
        return withInputStream(new InputStreamCall<Properties>() {

            public Properties call(InputStream in) throws Throwable {
                Properties p = new Properties();
                p.load(in);
                return p;
            }
        });
    }
    
    public Properties getContentAsProperties(final String encoding) {
        return withInputStream(new InputStreamCall<Properties>() {

            public Properties call(InputStream in) throws Throwable {
                InputStreamReader reader = new InputStreamReader(in, Charset.forName(encoding));
                Properties p = new Properties();
                p.load(reader);
                return p;
            }
        });
    }

    /**
     * Returns the file path
     * @return the file path
     */
    @Override
    public String toString() {
        return String.valueOf(file);
    }

    /**
     * Copies the contents of the folder denoted by the path of the underlying file into another folder
     * @param destinationFolder the folder where to copy the contents
     * @param notifiable an optional callback executed for each copied file
     */
    public void copyDirectoryContentsIntoDirectory(FileExt destinationFolder, final Function1<FileExt, ?> notifiable) {
        if(!file.exists()) return;
        if(!file.isDirectory()) return;
        if(!destinationFolder.file.isDirectory()) return;
        if(!destinationFolder.exists()) return;
        if(destinationFolder.file.equals(this.file)) return;
        
        final String destFolderPath = destinationFolder.getFile().getPath();
        final String path = file.getPath();
        this.scanTree(new VFunction1<FileExt>() { @Override protected void doApply(FileExt p) {
            String fileRelativePath = p.getRelativePath(path);
            String dirRelativePath = p.file.isDirectory() ? p.getRelativePath(path) : new FileExt(p.file.getParentFile()).getRelativePath(path);
            File destFile = new File(destFolderPath + fileRelativePath);
            File destDir = new File(destFolderPath + dirRelativePath);
            destDir.mkdirs();
            if(p.file.isFile()) try {
                p.transferDataTo(new FileExt(destFile));
            } catch(IOException ex) { 
                Logger.getLogger(FileExt.class.getName()).log(Level.SEVERE, "", ex);
            }
            if(notifiable != null) {
                notifiable.apply(p);
            }
        }});
    }
    
    /**
     * Transfers the data of this file to another file
     * @param dest the destination file
     * @throws IOException if a read/write exception occurs
     */
    public void transferDataTo(FileExt dest) throws IOException {
        FileChannel in = null;
        FileChannel out = null;
        try {
            in = new FileInputStream(file).getChannel();
            out = new FileOutputStream(dest.file).getChannel();
            long pos = 0;
            long rem = in.size();
            while(rem != 0) {
                long transferred = in.transferTo(pos, rem, out);
                pos += transferred;
                rem -= transferred;
            }
        } finally {
            try {
                if(in != null) in.close();
            } finally {
                if(out != null) out.close();
            }
        }
    }
    
    /**
     * Returns the relative path of this file given a parent path
     * @param parentPath the path of a parent directory
     * @return the relative path
     */
    public String getRelativePath(String parentPath) {
        return file.getPath().substring(parentPath.length());
    }

    /**
     * Traverses this directory calling a function for every file
     * @param fun the callback to execute on every file
     */
    public void scanTree(VFunction1<FileExt> fun) {
        if(!file.isDirectory() || !file.exists()) return;
        LinkedList<File> files = new LinkedList<File>();
        File[] children = file.listFiles();
        if(children != null) files.addAll(Arrays.asList(children));
        while(!files.isEmpty()) {
            File f = files.removeFirst();
            fun.apply(new FileExt(f));
            if(f.isDirectory()) {
                files.addAll(Arrays.asList(f.listFiles()));
            }
        }
    }

    public void mkDirs() {
        File path = file;
        if(file.isFile()) {
            path = file.getParentFile();
        }
        if(!path.exists()) {
            path.mkdirs();
        }
    }

    public FileExt generateUniqueSubfolder(String name) {
        Checks.True(file.isDirectory());
        Checks.True(file.exists());
        Checks.NotNull(name);
        Checks.False(name.isEmpty());
        File path = new File(file, name);
        int index = 0;
        while(path.exists()) {
            name = name + "_" + index;
            path = new File(file, name);
            index++;
        }
        path.mkdir();
        return new FileExt(path);
    }

    public void run() {
        try {
            Desktop.getDesktop().open(file);
        } catch (IOException ex) {
            Logger.getLogger(FileExt.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    public void print() {
        try {
            Desktop.getDesktop().print(file);
        } catch (IOException ex) {
            Logger.getLogger(FileExt.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    public FileExt getChild(String childFileName) {
        return new FileExt(new File(file, childFileName));
    }

    public void storeSerializableObject(final Serializable value) {
        withOutputStream(new OutputStreamCall<Void>() {

            public Void call(OutputStream out) throws Throwable {
                ObjectOutputStream o = new ObjectOutputStream(new BufferedOutputStream(out));
                o.writeObject(value);
                o.flush();
                o.close();
                return null;
            }
        });
    }

    public Object readSerializedObject() {
        return withInputStream(new InputStreamCall<Object>() {

            public Object call(InputStream in) throws Throwable {
                ObjectInputStream o = new ObjectInputStream(new BufferedInputStream(in));
                return o.readObject();
            }
        });
    }

    /** Copies the content of this file to the given file*/
    public void copyTo(FileExt file) {
        Logger.getLogger(FileExt.class.getName()).log(Level.INFO, "copy " + this + " to "+ file);
        file.write(getContentAsBytes().array());
    }

    /**
     * A function that can be used to execute code with the input stream created
     * by this file ext.
     * @param <T> the type of the return value
     */
    public static interface InputStreamCall<T> {

        /**
         * Executes this call
         * @param in the input stream
         * @return the result of the call
         * @throws Throwable to wrap exceptions that may happen during the call
         */
        T call(InputStream in) throws Throwable;
    }

    /**
     * A function that does something with an output stream and returns a value
     * @param <T> the type of the value returned by the call
     */
    public static interface OutputStreamCall<T> {

        /**
         * Executes the function and returns some value
         * @param out the output stream
         * @return the result of the call
         * @throws Throwable wrap exceptions that may happen during the call
         */
        T call(OutputStream out) throws Throwable;
    }

    /**
     * Creates a new file ext in the user folder
     * @param relativePath the path of the file, relative to the user folder
     * @return the requested file ext
     */
    public static FileExt newUserFolderFile(String relativePath) {
        return new FileExt(System.getProperty("user.home") + File.separator + relativePath);
    }


    /**
     * Creates a new file ext using the given relative or absolute path
     * @param path the path of the file
     */
    public FileExt(String path) {
        this.file = new File(path);
    }
    
    /**
     * List all the files in this folder that have an extension in the given set.
     * @param extensions the extension set to use to filter files. Can be null. If
     * null, all the files are returned
     * @return the files in this folder that have one of the required extensions
     */
    public ListExt<FileExt> listFiles(String... extensions) {
        LinkedListExt<FileExt> files = new LinkedListExt<FileExt>();
        if(file.isDirectory()) {
            File[] listFiles = file.listFiles();
            if(listFiles != null) {
                for (int i = 0; i < listFiles.length; i++) {
                    File f = listFiles[i];
                    FileExt ext = new FileExt(f);
                    if(ext.hasExtension(extensions)) files.add(ext);
                }
            }
        }
        return files;
    }

    /**
     * Returns the wrapped file
     * @return the wrapped file
     */
    public File getFile() {
        return file;
    }

    /**
     * Executes some call that uses a new input stream from this file. Closes
     * the stream after execution.
     * @param <T> the type of the returned value of the call
     * @param call the call to execute
     * @return the value returne by the call or null if an exception happens
     */
    public <T> T withInputStream(InputStreamCall<T> call) {
        T result = null;
        InputStream in = null;
        try {
            in = new FileInputStream(file);
            result = call.call(in);
        } catch(Throwable ex) {
            Logger.getLogger(FileExt.class.getName()).log(Level.SEVERE, "io exception", ex);
        } finally {
            if(in != null) {
                try {
                    in.close();
                } catch (IOException ex) {
                    Logger.getLogger(FileExt.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        }
        return result;
    }

    /**
     * Opens an output stream on this file, invokes the given call and closes
     * the stream.
     * @param <T> the type of the value returned by the call
     * @param call the function to execute
     * @return the value returned by the call or null if an exception happens
     */
    public <T> T withOutputStream(OutputStreamCall<T> call) {
        T result = null;
        OutputStream out = null;
        try {
            out = new FileOutputStream(file);
            call.call(out);
        } catch (Throwable ex) {
            Logger.getLogger(FileExt.class.getName()).log(Level.SEVERE, null, ex);
        } finally {
            if(out != null) try {
                out.close();
            } catch (IOException ex) {
                Logger.getLogger(FileExt.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        return result;
    }

    /**
     * Returns the parent of this file
     * @return the parent file
     */
    public FileExt getParent() {
        return new FileExt(getFile().getParentFile());
    }

    /**
     * True if the extension of this file matched the given one. Case insensitve
     * @param ext the extension to check
     * @return true if this file ends with the given extension. A . is prefixed
     * if missing in ext.
     */
    public boolean hasExtension(String ext) {
        if(!ext.startsWith(".")) ext = "." + ext;
        return file.getName().toLowerCase().endsWith(ext.toLowerCase());
    }

    /**
     * True if the extension of this file matches one the extensions in the
     * provided array
     * @param ext the array of extensions to check
     * @return tue if hasExtension(x) for at least one x in ext
     */
    public boolean hasExtension(String[] ext) {
        for (int i = 0; i < ext.length; i++) {
            if(hasExtension(ext[i])) return true;
        }
        return false;
    }

    /**
     * Returns the contents of the file as a string in the given charset. If
     * charset is null the platform default one is used.
     * @param charsetName the name of the charset to use or null to use the
     * platform default charset
     * @return the content of the file or null if an exception occurs.
     */
    public String getContentAsString(String charsetName) {
        String result = null;
        ByteBuffer buffer = getContentAsBytes();
        if(buffer != null) {
            Charset charset = charsetName == null ? Charset.defaultCharset() : Charset.forName(charsetName);
            result = charset.decode(buffer).toString();
        }
        return result;
    }

    /**
     * Returns the content of the file as a byte buffer
     * @return the content of the file as a byte buffer or null if an exception occurs
     */
    public ByteBuffer getContentAsBytes() {
        if(!getFile().exists()) return ByteBuffer.allocate(0);
        ByteBuffer result = null;
        FileInputStream stream = null;
        try {
            FileChannel channel = (stream = new FileInputStream(getFile())).getChannel();
            ByteBuffer buffer = ByteBuffer.allocate((int) channel.size());
            int maxLoops = 100000;
            while(buffer.hasRemaining() && --maxLoops > 0) {
                channel.read(buffer);
            }
            buffer.flip();
            result = buffer;
        } catch(IOException ex) {
            Logger.getLogger(FileExt.class.getName()).log(Level.SEVERE, null, ex);
        } finally {
            if(stream != null) try {
                stream.close();
            } catch (IOException ex) {
                Logger.getLogger(FileExt.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        return result;
    }

    /**
     * Return the simple name of the wrapped file with no extension
     * @return the simple name of the wrapped file
     */
    public String getUnqualifiedName() {
        String name = file.getName();
        int dot = name.lastIndexOf(".");
        if(dot >= 0) {
            name = name.substring(0, dot);
        }
        return name;
    }

    /**
     * Returns a new FileExt with the same file path of this one but a different
     * extension. Eg: path/name.ext becomes path/name.txt
     * @param newExtension the new extension (. prepended if missing)
     * @return  a new file with the same name of this one but a different extension
     */
    public FileExt replaceExtension(String newExtension) {
        if(!newExtension.startsWith(".")) newExtension = "." + newExtension;
        return new FileExt(getFolderPath() + File.separator + getUnqualifiedName() + newExtension);
    }

    /**
     * Return the url of the file. Throws a runtime exception if a MalformedURLException happens
     * @return the url
     */
    public URL getURL() {
        try {
            return getFile().toURI().toURL();
        } catch (MalformedURLException ex) {
            throw new RuntimeException(ex);
        }
    }

    /**
     * Cheks if the file has the given extension. If not, applies it to the
     * wrapped file
     * @param extension the extension to check. If the . is missing, prepends it.
     * @return this
     */
    public FileExt applyExtension(String extension) {
        String fileName = file.getName().toLowerCase();
        if(!extension.startsWith(".")) extension = "." + extension;
        String ext = extension.toLowerCase();
        if(!fileName.endsWith(ext)) {
            return new FileExt(new File(file.getPath() + extension));
        } else {
            return this;
        }
    }

    /**
     * Returns an output stream for this file
     * @param buffered true if the stream has to be buffered
     * @return the requested stream
     * @throws IOException if the creation fails
     */
    public OutputStream getOutputStream(boolean buffered) throws IOException {
        FileOutputStream out = new FileOutputStream(getFile());
        if(buffered) {
            return new BufferedOutputStream(out);
        } else {
            return out;
        }
    }

    /**
     * Writes the given byte array on this file
     * @param bytes the bytes to write
     * @throws RuntimeException wraps the io exception
     */
    public void write(byte[] bytes) throws RuntimeException {
        OutputStream out = null;
        try {
            out = getOutputStream(true);
            out.write(bytes);
            out.flush();
            out.close();
        } catch(IOException ex) {
            throw new RuntimeException(ex);
        } finally {
            if(out != null) try {
                out.close();
            } catch (IOException ex) {
                Logger.getLogger(FileExt.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        
    }

    /**
     * Returns the path of the folder of this file
     * @return the path of the folder of this file
     */
    public String getFolderPath() {
        return file.getParent();
    }
}
